#!/usr/bin/ruby
# -*- coding: utf-8 -*-
# -------------------------------------------------------------------------------
#    DBGP-Tunnel client implementaion in Ruby
#    http://code.google.com/p/dbgp-tunnel/
#
#    Copyright (c) 2011 HANAI Tohru <hanai.tohru@gmail.com>
#
#    Contributor(s): none yet.
# -------------------------------------------------------------------------------
#   This software is provided 'as-is', without any express or implied
#   warranty. In no event will the authors be held liable for any damages
#   arising from the use of this software.
#
#   Permission is granted to anyone to use this software for any purpose,
#   including commercial applications, and to alter it and redistribute it
#   freely, subject to the following restrictions:
#
#   1. The origin of this software must not be misrepresented; you must not
#      claim that you wrote the original software. If you use this software
#      in a product, an acknowledgment in the product documentation would be
#      appreciated but is not required.
#   2. Altered source versions must be plainly marked as such, and must not be
#      misrepresented as being the original software.
#   3. This notice may not be removed or altered from any source distribution.
# -------------------------------------------------------------------------------

require 'optparse'
require 'socket'
require 'logger'

Version = '0.2'
$logger = Logger.new(STDOUT)
$logger.level = Logger::INFO
STDOUT.sync = true

# commmands/responses
REQUEST_TUNNEL        = "tunnel"
REQUEST_SESSION       = "session"
REQUEST_START         = "start"
RESPONSE_ACCEPT       = "accept"
RESPONSE_OK           = "ok"
RESPONSE_ERROR        = "error"

# errors
ERROR_INVALID_COMMAND  = "invalid_command"
ERROR_SYNTAX           = "syntax_error"
ERROR_IDEKEY_IN_USE    = "idekey_in_use"
ERROR_INVALID_IDEKEY   = "invalid_idekey"
ERROR_UNKNOWN_SESSION  = "unknown_session"

class TCPSocket
  def send(request, param = nil)
    if param.nil?
      write("#{request}\0")
    else
      write("#{request} #{param}\0")
    end
  end

  def recv()
    (message, param) = gets("\0").chomp("\0").split(' ', 2)
    param = '' if param.nil?
    [message, param]
  end
end

class DbgpTunnelClient
  def initialize(ip, port, idekey)
    @logger = $logger

    @tunnel_addr = ip
    @tunnel_port = port
    
    @logger.debug("connect to #{ip}:#{port}")
    @tunnel_socket = TCPSocket.open(ip, port)
    @logger.debug("connection established on #{getSocketName(@tunnel_socket)}")

    @logger.debug("use ide key '#{idekey}'")

    @tunnel_socket.send(REQUEST_TUNNEL, idekey)
    (response, param) = @tunnel_socket.recv

    case response
    when RESPONSE_ACCEPT
      @logger.debug('connection establiashed')
      return
    when RESPONSE_ERROR
      error = param
    else
      error = "the tunnel server returns unknown response: '#{response} #{param}'"
    end

    errorExit(error)
  end

  def errorExit(error)
    @logger.error(error)
    exit
  end

  def getSocketName(socket)
    addr = socket.addr
    addr.shift
    addr.join(':')
  end

  def startDbgpSession(dbgp_addr, dbgp_port, session_key)
    @logger.debug('starting a new dbgp session')

    Thread.start do 
      begin
        dbgp_socket = TCPSocket.open(dbgp_addr, dbgp_port)
        #dbgp_socket.binmode
        tunnel_socket = TCPSocket.open(@tunnel_addr, @tunnel_port)
        #tunnel_socket.binmode
        tunnel_socket.send(REQUEST_START, session_key)
        tunnel_socket.recv

        def tunnel_socket.kind
          'response session'
        end

        def dbgp_socket.kind
          'command session'
        end

        socks = [dbgp_socket, tunnel_socket]
        while socks.length
          nsock = IO.select(socks)
          next if nsock.nil?

          for s in nsock[0]
            opposite = (s == tunnel_socket ? dbgp_socket : tunnel_socket)

            if s.eof?
              @logger.debug("#{s.kind} has disconnected")
              s.close
              socks.delete(s)
              opposite.close
              socks.delete(opposite)
              break
            end

            if s == dbgp_socket
              message = s.gets("\0")
            else
              message = s.gets("\0") + s.gets("\0")
            end
            @logger.debug("#{s.kind}> #{message}") if s == dbgp_socket
            @logger.debug("opposite: #{opposite.kind}")
            opposite.write(message)
          end
        end
      rescue Exception => e
        @logger.debug("dbgp session exception: #{$!}")
        puts e.backtrace
      end
    end
  end
  
  def run(dbgp_addr, dbgp_port)
    socks = [@tunnel_socket]
    listener = nil

    while true
      nsock = IO.select(socks)
      for s in nsock[0]
        case s
        when @tunnel_socket
          if s.eof?
            s.close
            @logger.info("tunnel is closed")
            return
          end

          (message, param) = s.recv
          @logger.debug("tunnel| #{message} #{param or ''}")

          case message
          when REQUEST_SESSION
            startDbgpSession(dbgp_addr, dbgp_port, param)
          end
        end
      end
    end
  end

  def readResponse(socket)
    response = s.gets("\0") + s.gets("\0")
    @logger.debug(response)
    return response
  end
end

def main
  tunnel_ip = nil
  tunnel_port = 9100
  dbgp_addr = 'localhost'
  dbgp_port = 9000
  idekey = 'default'

  opt = OptionParser.new
  opt.on('-t', '--tunnel=IP:PORT', 'tunnel server address and port(required)') do |v|
    (tunnel_ip, tunnel_port) = v.split(':')
  end

  opt.on('-s', '--session=ADDR:PORT',
         "interface address and port for a DBGP session (default: #{dbgp_addr}:#{dbgp_port})") do |v|
    (dbgp_addr, dbgp_port) = v.split(':')
  end

  opt.on('-k', '--idekey=KEY', "ide key (default: 'default')") do |v|
    idekey = v
  end

  opt.on('-d', '--debug', 'output log for debug') do |v|
    $logger.level = Logger::DEBUG
  end

  opt.parse!(ARGV)
  if tunnel_ip == nil
    puts opt.help
    exit
  end

  client = DbgpTunnelClient.new(tunnel_ip, tunnel_port, idekey)
  client.run(dbgp_addr, dbgp_port)
end

main()

